# -*- coding: utf-8 -*-
"""
    Name: Non-Recurrent Shuffler
    Version: 2013.03.18
    Author: andrew_berge
    Description: Shuffles playlists so that the same artist doesn't play twice in a row. Also supports other tags, such as genre.

    Known bugs:
        None for now.
"""


# Define Functions
# Function descriptions assume we're sorting the *Artist* tag. For simplicity's sake.


def promptInputFile():
    # Prompt user for the input file.

    InputFile = raw_input("Enter input playlist file (m3u or m3u8).\n-->")
    
    # If there was no Input, we'll search the current folder for one.
    if InputFile == "":
        print ("No input. Searching current folder for files...")
        # Set the filetypes permitted, and turn InputFile into an empty list.
        SupportedFiles = ["*.m3u8", "*.m3u"]
        InputFile = []
        # Search for a playlist in the current folder
        import glob
        for FileType in SupportedFiles:
            InputFile.extend(glob.glob(FileType))
        # If a playlist was found. Return it.
        if len(InputFile) > 0:
            InputFile = InputFile[0]
            print ("Using " + InputFile)
        else:
            # If not, return an empty string.
            print ("None found.")
            InputFile = ""
    # If the user entered something.
    else:
        # Check if the file exists.
        if os.path.exists(InputFile):
            # If it does, set the working directory to it's location.
            os.chdir(os.path.dirname(os.path.realpath(InputFile)))
        else:
            # If it doesn't, return an empty string
            print ("Input file does not exist or cannot be accessed.")
            InputFile = ""
        
    return InputFile


def promptOutputFile(fileext):
    # Prompt user for an output file. Revert to default if none is given. Set the file extension to the same as the input file.
    # Input: The file extension this file must have.

    OutputFile = raw_input("\nEnter output file.\n-->")
    # If the given filename was blank, revert to the default name.
    if OutputFile == "":
        if not OutputFile.endswith(fileext):
            OutputFile = "NewPlaylist" + FileExt
            print ("No input. Using default: NewPlaylist" + FileExt)
    else:
        # If the given filename doesn't end with the proper extension, add it.
        if not OutputFile.endswith(fileext):
            OutputFile += FileExt
        
    return OutputFile


def promptVarTag():
    # Prompt the user for a tag to sort by. Revert to default if none is given.

    VarTag = raw_input("\nEnter tag to sort by.\n-->")
    # If no tag was given, revert to default.
    if VarTag == "":
        VarTag = "Artist"
        print ("No input. Using default: Artist")
    return VarTag


def readM3U(FileName):
    # Opens and reads a given file. File should be an M3U, or at least contain a list of compatible files.
    # Any extended info or dead links are ignored.
    # Input: Filename.

    # Open the file. Copy each line. Close the file.
    global Encoding
    m3ufile = open(FileName)
    if FileName.endswith(".m3u8"):
        Encoding = "utf-8-sig"
    elif str(m3ufile.encoding) != "None":
        Encoding = m3ufile.encoding
    OpenFile = m3ufile.read().decode(str(Encoding))
    OpenFile = OpenFile.splitlines()
    m3ufile.close()
    if Encoding == "utf-8-sig":
        Encoding = "utf-8"

    # The list DeadLinks will contain any files that can't be found.
    DeadLinks = []

    # Remove linebreaks and any whitespace there may be in each line.
    for line in range(len(OpenFile)):
        OpenFile[line] = OpenFile[line].rstrip()

    # Delete lines that are unnecessary or will cause errors.
    for line in OpenFile[:]:
        # Search for and delete extended information present in some M3U files.
        if line.startswith("#"):
            OpenFile.remove(line)
        # Search for any files that can't be accessed. Add them to DeadLinks.
        elif not os.path.exists(line):
            DeadLinks.append(line)
            OpenFile.remove(line)

    # If DeadLinks contains any files, warn the user.
    if len(DeadLinks) > 0:
        print ("\n" + str(len(DeadLinks)) + " of " + str(len(OpenFile)+len(DeadLinks)) + " files could not be found:")
        for line in DeadLinks:
            print (line)
        raw_input ("\nPress enter to continue...")

    # By now OpenFile contains a list of only accessible, compatible files.
    return OpenFile


def getTags(filename, tag):
    # Returns the value of a given tag in a given file.
    # Input: Filename (including path and extension), desired tag.

    var = File(filename, easy=True)
    try: vartag = var[tag]
    except KeyError:
        vartag = "[MISSINGTAG]"
    except TypeError:
        vartag = "[UNSUPPORTEDFILE]"
    return vartag


def populateDictFileTag(filelist, tag):
    # Returns a dictionary with filenames as keys and each file's *Artist* as values
    # Input: A list of files, desired tag.
    # Notes: Duplicate files are overwritten.

    sys.stdout.write("\nPlease wait. Reading tags")
    loading = 1
    DictFileTag = {}
    for f in filelist:
        DictFileTag[f] = "".join(getTags(f, tag))
        # Three dots will appear as the tags are read, each represents 25%.
        if filelist.index(f) >= (len(filelist)/4)*loading:
            sys.stdout.write(".")
            loading += 1
    print ("")
    return DictFileTag


def populateDictTagCount(filelist, FileTag):
    # Returns a dictionary with *Artist*s as keys and the number of tracks each as values.
    # Input: FileList and DictFileTag.

    TagCount = {}
    for f in filelist:
        if FileTag[f] in TagCount:
            TagCount[FileTag[f]] += 1
        else:
            TagCount[FileTag[f]] = 1
    return TagCount


def getDictMaxValue(Dict):
    # Returns the key with the highest value in a dictionary.
    # Input: A dictionary.

    MaxValue = 0
    for Key in Dict:
        if Dict[Key] > MaxValue:
            MaxValue = Dict[Key]
            MaxKey = Key
    return MaxKey


def checkNumbers(TagCount):
    # Check if a playlist can be made without two consecutive tracks with the same *Artist*.
    # If not, ask for confirmation to continue.
    # Returns either True or False.
    # Input: DictTagCount. A dictionary with tags as keys and the number of each as values.
    # Notes: FileList must exist.

    # Start by printing some information about the playlist.
    # Print a list of each artist and how many times each appears, sorted by popularity.
    print ("")
    # Create a copy of TagCount. Sort it from highest to lowest and print. The copy is destroyed in the process.
    DispTagCount = TagCount.copy()
    while len(DispTagCount) > 0:
        PopTag = getDictMaxValue(DispTagCount)
        print (PopTag.encode("ascii", "replace") + ": " + str(DispTagCount[PopTag]))
        del DispTagCount[PopTag]

    # Find the most popular tag and how often it appears, print more information.
    PopTag = getDictMaxValue(TagCount)
    PopNum = TagCount[PopTag]
    print ("\nTotal tags: " + str(len(TagCount)))
    print ("Most popular tag is: " + PopTag.encode("ascii", "replace"))
    print ("Number of tracks with tag: " + str(PopNum))
    print ("Total tracks in playlist: " + str(len(FileList)))

    # To be able to sort without identical consecutive tags, the most popular tag should have a count equal or lower to half the total playlist. The list FileList was created earlier.
    if PopNum <= (len(FileList)/2):
        # All numbers check out. Ready to create playlist.
        return True
    else:
        # There are too many of a certain tag. Ask the user if we should continue.
        print ("\nMore than half the tracks have the tag " + PopTag.encode("ascii", "replace") + ".")
        confirm = raw_input("Continue anyway? Y/N\n").lower()
        if  confirm == "y":
                    # The user said yes. There will be consecutive files with the same tag, but we're continuing.
                    return True
        elif confirm == "n":
            # The user said no. Abort.
            print ("Operation aborted.")
            return False
        elif confirm == "":
            print ("No input. Continuing...")
            return True
        else:
            #The user wrote something other than the clearly stated possible answers. Abort.
            print ("\"" + confirm + "\" is not a recognized response. Operation aborted.")
            return False


def createNewPlaylist(TagCount,FileTag):
    # Creates and returns the new playlist.
    # Input: DictTagCount, DictFileTag.
    # Notes: FileList must exist.

    # Create a disposable copy of TagCount.
    DispTagCount = TagCount.copy()
    # ListPos will be used to keep our position as we iterate through the playlist later.
    ListPos = 0
    # Create the new playlist, this list will be returned at the end of the function.
    NewPlaylist = []
    # Get the playlist length. ListRest will be used later, telling us how many empty spaces are left in the new playlist.
    PlaylistLength = len(FileList)
    ListRest = PlaylistLength
    # Create a new playlist with the proper length, filled with blank entries
    while ListPos < PlaylistLength:
        NewPlaylist.append("[NONE]")
        ListPos += 1

    # While there are still entries in DispTagCount. The following loop will be executed once for every artist.
    while len(DispTagCount) > 0:
        #Determine the current most popular *Artist* in the list.
        PopTag = getDictMaxValue(DispTagCount)
        # The tag will be placed and spread evenly in the remaining blank spaces of the playlist. Determine how spaced they must be.
        Frequency = float(ListRest)/float(TagCount[PopTag])
        ListIter = 0
        TagIter = 0
        #Choose a random starting point in the playlist, otherwise the most popular *Artist* would always be the first track.
        ListPos = int(round(random.random() * (PlaylistLength-1)))
        # TagIter starts at 0 and will be increased by one. The following loop will be executed once for every time the current *Artist* must appear.
        while TagIter < TagCount[PopTag]:
            # Unfortunately, the current sorting strategy has a few exceptions, and will sometimes have repeated tags. This if statement takes care of that. See the else statement for more info.
            if Frequency > 2 or DispTagCount == TagCount:
                # if the current position in the new playlist is empty. (ListPos is the true position in the playlist.)
                if NewPlaylist[ListPos] == "[NONE]":
                    # Imagine a playlist made of all remaining empty positions, ListIter is our position in that playlist. Check if this position is spaced enough from the last.
                    if ListIter == round(TagIter * Frequency):
                        # Insert the current *Artist* into that position, increase TagIter by 1.
                        NewPlaylist[ListPos] = PopTag
                        TagIter += 1
                    # Whether or not we inserted an *Artist*, the current position in the empty playlist is ListIter. The next empty space we meet will be ListIter+1.
                    ListIter += 1
            # Certain tag number combinations (Such as 10,10,1) will lead to repeated tags, this tries to avoid that by checking the empty line's surroundings.
            else:
                # if the current position in the new playlist is empty. (ListPos is the true position in the playlist.)
                # Also make sure the tracks before and after (in the real playlist) are not by this *Artist*.
                
                if NewPlaylist[ListPos] == "[NONE]" and NewPlaylist[ListPos-1] != PopTag and NewPlaylist[(ListPos + 1) % len(NewPlaylist)] != PopTag:
                    # Imagine a playlist made of all remaining empty positions, ListIter is our position in that playlist. Check if this position is spaced enough from the last.
                    if ListIter == round(TagIter * Frequency):
                        # Insert the current *Artist* into that position, increase TagIter by 1.
                        NewPlaylist[ListPos] = PopTag
                        TagIter += 1
                    # Whether or not we inserted an *Artist*, the current position in the empty playlist is ListIter. The next empty space we meet will be ListIter+1.
                    ListIter += 1
                
            # If we've reached the end of the true playlist, start back at the beginning, otherwise move on to the next position.
            if ListPos >= PlaylistLength-1:
                ListPos = 0
            else:
                ListPos += 1
        # The while loop is over. all positions for one *Artist* have been placed. Recalculate the number of blank spaces for then next *Artist*.
        ListRest -= TagCount[PopTag]
        # Remove the *Artist* we just placed from the dictionary. We can calculate the most popular *Artist* again to find the next one.
        del DispTagCount[PopTag]

    # The first while loop is over. NewPlaylist is now full, an *Artist* in each space. Now we replace each *Artist* with a corresponding file.
    # Create a disposable FileList
    DispFileList = FileList
    # For each indice in the NewPlaylist.
    for ind in range(len(NewPlaylist)):
        # Check each file in FileList. If it has the right tag, put it in the playlist, delete it from FileList, then break, moving on to the next indice in NewPlaylist.
        for line in range(len(DispFileList)):
            if FileTag[DispFileList[line]] == NewPlaylist[ind]:
                NewPlaylist[ind] = DispFileList[line]
                del DispFileList[line]
                break
    
    # NewPlaylist is now a list of files, sorted semi-randomly so that *Artist* is never twice in a row. Return the list.
    return NewPlaylist


def checkDoubles(filelist,FileTag):
    # Check the final list of files for recurrent *Artists*.
    # Returns 1 if any are found, 0 if none
    # Input: NewPlaylist and DictFileTag

    # Recurrent is the number of times there is tag repitition in the list.
    Recurrent = 0
    # Iterate through the filelist
    for indice in range(len(filelist)):
        # If the tag for this item matches the one for the previous item, add 1 to Recurrent.
        if FileTag[filelist[indice]] == FileTag[filelist[indice-1]]:
            Recurrent += 1

    if Recurrent > 0:
        return 1
    else:
        return 0
    



def writeToFile(FileList, FileName):
    # Takes a list of files and saves them to a file.
    # Input: a list 'FileList' and a filename 'FileName'.
    # Notes: Filename must be a string and should include a file extension (usually ".m3u")

    # Create and open a new file for writing in.
    var = codecs.open(FileName, "w", Encoding)
    # Write FileList to the new file, with linebreaks.
    for i in FileList:
        var.seek(0,2)
        var.write(i + "\n")
    # Close the file.
    var.close()


# FUNCTIONS END HERE


#Import Modules
import os.path
import random
from mutagen import File
import sys
import codecs

# Set the encoding used to system default. This may be changed later.
Encoding = sys.stdout.encoding

# Start by asking for an input file. If the file exists, continue with everything else.
InputFile = promptInputFile()
if InputFile != "":
    # Get InputFile's Extension.
    dispvar, FileExt = os.path.splitext(InputFile)
    # Ask for an output file and tag to sort by.
    OutputFile = promptOutputFile(FileExt)
    VarTag = promptVarTag()
    # Open the first playlist.
    FileList = readM3U(InputFile)
    # Shuffle the files.
    random.shuffle(FileList)
    # Read the tags. Create needed dictionaries.
    DictFileTag = populateDictFileTag(FileList, VarTag)
    DictTagCount = populateDictTagCount(FileList, DictFileTag)
    print ("Done. Counting...")
    # Check the numbers, then proceed if everything checks out.
    if checkNumbers(DictTagCount) == True:
        print ("\nReorganizing playlist...")
        NewPlaylist=createNewPlaylist(DictTagCount,DictFileTag)
        print ("Double checking for recurrent tags...")
        if checkDoubles(NewPlaylist, DictFileTag) == 1:
            print ("\nWARNING: Recurrent tags detected.\n")
            Error = True
        else:
            print ("All good.")
            Error = False
        print ("Creating new playlist file...")
        writeToFile(NewPlaylist,OutputFile)
        if Error == True:
            print ("Finished with errors.")
        else:
            print ("Finished.")

# All done.
raw_input ("\nPress enter to end.")
